package com.mhy.common.util;

import com.google.common.collect.Collections2;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;

/**
 * 敏感词过滤工具类，基于DFA算法
 * @auther mahaiyuan
 * @create 2020-04-14 16:25
 */
@Slf4j
public class WordFilterUtil {

    private static final String KEY_FILE = "keys.txt";
    private volatile Node root = new Node('R'); //根节点
    private static final WordFilterUtil instance = new WordFilterUtil();

    private WordFilterUtil() {
        init();
    }

    public static WordFilterUtil getInstance() {
        return instance;
    }

    /**
     * 检测文字中是否包含敏感
     * @param content
     * @return
     */
    public boolean hasSensitiveWord(String content) {
        if (StringUtils.isBlank(content)) {
            return false;
        }
        char[] chars = content.toCharArray();
        Node curNode = root;
        for (int i = 0; i < chars.length; i++) {
            curNode = curNode.nodeMap.get(String.valueOf(chars[i]));
            if (null == curNode) {  //当前字符不在敏感词库
                curNode = root; //重新从根节点开始遍历
                continue;
            }
            // 当前字符在敏感词库
            if (curNode.isEnd) {    //节点已到叶子节点
                return true;
            }
        }
        return false;
    }

    /**
     * 获取一段文字中所有的敏感词[最小敏感词原则]<br/>
     * 比如敏感词库中包含冰毒、毒品，文字中有xxx冰毒品xxx，此时只获取出冰毒
     * @param content
     * @return
     */
    public Set<String> getSensitiveWordsMin(String content) {
        if (StringUtils.isBlank(content)) {
            return null;
        }
        Node curNode = root;
        Set<String> rs = new HashSet<>();   //记录敏感词
        char[] chars = content.toCharArray();
        int startIndex = 0;
        for (int i = 0; i < chars.length; i++) {
            curNode = curNode.nodeMap.get(String.valueOf(chars[i]));
            if (curNode == null) {  //当前字符不在敏感词中
                curNode = root;
                startIndex++;
                continue;
            }
            // 当前字符在敏感词库中
            if (curNode.isEnd) {    //当前字符在敏感词库结尾
                rs.add(new String(chars, startIndex, (i - startIndex + 1)));
                curNode = root;
                startIndex = i + 1;
            }
            //当前字符不在敏感词库结尾继续循环即可
        }
        return rs;
    }

    /**
     * 初始化敏感词库
     */
    private void init() {
        List<String> words = getSensitiveWords();
        if (null == words || words.isEmpty()) {
            return;
        }
        for (String line : words) {
            char[] chars = line.toCharArray();
            if (chars == null) {
                continue;
            }
            insertNodes(root, chars);
        }
    }

    private void insertNodes(Node curNode, char[] chars) {
        for (int i = 0; i < chars.length; i++) {
            Node nextNode = getNextNode(curNode, chars[i]);
            if (nextNode == null) { //下一跳节点不存在
                boolean isEnd = (i == (chars.length - 1)) ? true : false;
                nextNode = new Node(chars[i], isEnd);
                curNode.nodeMap.put(String.valueOf(chars[i]), nextNode);
            } else {    //下一跳节点存在
                /**
                 * 下一跳节点存在有两种情况
                 * 1. 当前敏感字没到尾，不需要做任何处理，继续获取下一跳节点
                 * 2. 当前敏感字到尾，这种又分两种情况
                 * 2.1 当前敏感字到尾，敏感字树到叶子节点，不需要处理
                 * 2.2 当前敏感字到尾（例如当前敏感字符串为"毒品"），敏感词库已存在"毒品大全”
                 * 这种情况下此处采用的策略是使用最小匹配原则，匹配到毒品就算触发敏感词
                 */
                if (i == chars.length - 1) {
                    nextNode.isEnd = true;
                    nextNode.nodeMap = null;
                    curNode.nodeMap.put(String.valueOf(chars[i]), nextNode);
                }
            }
            curNode = nextNode;
        }
    }

    /**
     * 获取当前节点的下一跳节点
     * @param curNode
     * @param ch
     * @return
     */
    private Node getNextNode(Node curNode, char ch) {
        if (curNode.isEnd) {
            return null;
        }
        return curNode.nodeMap.get(String.valueOf(ch));
    }


    /**
     * 获取敏感词列表
     * @return
     */
    private List<String> getSensitiveWords() {
        List<String> words = new ArrayList<>();
        BufferedReader reader = new BufferedReader(
                new InputStreamReader(ClassLoader.getSystemResourceAsStream(KEY_FILE)));
        try {
            String line = null;
            while ((line = reader.readLine()) != null) {
                words.add(line);
            }
            return words;
        } catch (Exception e) {
            log.error("getSensitiveWords exception.");
            e.printStackTrace();
        } finally {
            try {
                reader.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 敏感词节点类
     */
    private class Node {
        private char c; //当前节点上的敏感字符
        private boolean isEnd;  //是否是最后一个字符
        private Map<String, Node> nodeMap = new HashMap<>();    //该敏感字符后所有的敏感词字符

        public Node(char c) {
            this.c = c;
            this.isEnd = false;
        }

        public Node(char c, boolean isEnd) {
            this.c = c;
            this.isEnd = isEnd;
        }
    }
}
